<?php
class login
{
/*
* Version 0.8 - Still requires logout method and a method to authenticate users on subsiquent pages after 
* inisial login.  If setLoginSession lastActive is false log the user out.

* Below is a list of all the elements in the array that need to be passed as an argument to the cosntructor of 
* this class.  Below that is an example of how to code the array, you can copy and paste the example and just 
* change the values.

* db_table (string) - Table that contains the user details. - REQUIRED
* 
* username_field (string) - The table field that contains the username. - REQUIRED
* 
* pasword_field (string) - The table field that contains the password. - REQUIRED
* 
* loginTimestamp_field (string) - The table field used to record the users last login. - OPTIONAL
* 
* uniqueId_field (string) - The table field that holds a unique ID used to constantly validate users after 
    they've logged in, the ID is generated by a method in this class. - OPTIONAL
*  
* emailAuth_field (boolean) - The table field which has a 1 (authenticated) or 0 (not authenticated) to signify if the user has been 
    authenticated, normally with an email sent to their address with a link they have to click to confirm the did sign up for 
    this. - OPTIONAL
* 
* forceCookies (boolean) - This sets if cookies must be used (true) or not (false), it is far more secure to 
    enable this but users who have them disabled will be unable to login - REQUIRED
*
* forceCookies_page (string) - The path the user should be taken to if they don't have cookies enabled and 
    the forceCookies value is set to true - OPTIONAL
*
* session_path (string) - Path where sessions are saved, only required on a shared web server for security. - OPTIONAL
* 
* session_timeout (int) - This is the time (in seconds) a user can stay inactive for before being 'kicked 
    out'. - OPTIONAL (default = no limit)
*
* timeout_page (string) - The page & path the user is sent to if they have been inactive to long. - OPTIONAL but 
    requied if session_timeout has a value set
*
* hash (string) - Type of hash the password is stored in, "print_r(hash_algos());" lists all hashing options. - OPTIONAL (default is no hash)
*
* pepper (string) - Used to secure passwords by adding a static string (always the same for every user) to 
    the end of the password the user entered. Once set NEVER CHANGE! - OPTIONAL

* EXAMPLE
* $login_args = array('db_table' => 'login', 
*                       'username_field' => 'username', 
*                       'password_field' => 'password', 
*                       'loginTimestamp_field' => 'timestamp', 
*                       'uniqueId_field' => 'unique', 
*                       'emailAuth_field' => 'authenticated', 
*                       'forceCookies' => true, 
*                       'forceCookies_page' => 'no_cookies.php', 
*                       'session_path' => NULL, 
*                       'session_timeout' => 0, 
*                       'timeout_page' => NULL, 
*                       'hash' => NULL, 
*                       'pepper' => NULL);
*/

    private $db_table;
    private $username_field;
    private $password_field;
    private $loginTimestamp_field;
    private $uniqueId_field;
    private $emailAuth_field;
    private $forceCookies;
    private $forceCookies_page;
    private $session_path;
    private $session_timeout;
    private $timeout_page;
    private $hash;
    private $pepper;

    private $sessionName = "cllQgkc33";
    private $unique_id = NULL;

/*
* This filters and does some basic validation on the information sent to it in the $args array, these values 
* are used to set the variables above. This also regenerates the session ID and forces cookies to be used if the 
* option is set
* 
* $args (array) See comments at the start of this class - REQUIRED
*/
    public function __construct($args)
    {
        //Loops through all the arguments in the $args array
        foreach($args as $key => $value)
        {
            switch($key)
            {
                case 'db_table':
                    //Need to add an expression to validate table names
                    $this->db_table = $value;

                    break;
                case 'username_field':
                    //Need to add an expression to validate table names
                    $this->username_field = $value;

                    break;
                case 'password_field':
                    //Need to add an expression to validate table names
                    $this->password_field = $value;

                    break;
                case 'loginTimestamp_field':
                    //Need to add an expression to validate table names
                    $this->loginTimestamp_field = $value;

                    break;
                case 'uniqueId_field':
                    //Need to add an expression to validate table names
                    $this->uniqueId_field = $value;

                    break;
                case 'emailAuth_field':
                    //Need to add an expression to validate table names
                    $this->emailAuth_field = $value;

                    break;
                case 'session_path':
                    $this->session_path = filter_var($value, FILTER_SANITIZE_URL);

                    if(is_null($this->session_path) || empty($this->session_path)){ $this->session_path = NULL; }
                    elseif($this->session_path === false){ echo 'Could not use session path entered in $args'; exit; }
                    else{ session_save_path($this->session_path); }

                    break;
                case 'session_timeout':
                     $this->session_timeout = filter_var($value, FILTER_VALIDATE_INT);

                    if(is_null($this->session_timeout) || empty($this->session_timeout) || $this->session_timeout === 0){ $this->session_timeout = NULL; }
                    elseif($this->session_timeout === false){ echo 'Enter an integer value for the session timeout in $args'; exit; }
                    elseif(!isset($args['timeout_page'])){ echo 'You have set the session timeout value to true, you must also specify the timeout page value in $args.'; exit; }

                    break;
                case 'timeout_page':
                    $this->timeout_page = filter_var($value, FILTER_SANITIZE_URL);

                    if(is_null($this->timeout_page) || empty($this->timeout_page)){ $this->timeout_page = NULL; }
                    elseif(!is_null($this->timeout_page) && $this->timeout_page === false){ echo 'Please enter a valid path for the session timeout page in $args'; exit; }

                    break;
                case 'hash':
                    $this->hash = $value;

                    if(is_null($this->hash) || empty($this->hash)){ $this->hash = NULL; }
                    elseif(!in_array($this->hash, hash_algos())){ echo "The has algorithm you entered (".$value.") is not available on your server, use print_r(hash_algos()) to view a list of all available algorithms"; exit; }

                    break;
                case 'pepper':
                    $this->pepper = filter_var($value, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH);

                    if(is_null($this->pepper) || empty($this->pepper)){ $this->pepper = NULL; }
                    elseif($this->pepper === false){ echo 'Could not use the pepper entered in $args'; exit; }

                    break;
                case 'forceCookies':
                    $this->forceCookies = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);

                    if($this->forceCookies !== true && $this->forceCookies !== false){ echo 'Please set the force cookies value in $args'; exit; }
                    elseif(!isset($args['forceCookies_page'])){ echo "You have set force cookies value to true, you must also specify the force cookies page value."; exit; }

                    break;
                case 'forceCookies_page':
                    $this->forceCookies_page = filter_var($value, FILTER_SANITIZE_URL);

                    if(is_null($this->forceCookies_page) || empty($this->forceCookies_page)){ $this->forceCookies_page = NULL; }
                    elseif(!is_null($this->forceCookies_page) && $this->forceCookies_page === false){ echo "Please enter a valid path for the no cookie page, you entered ".$value; exit; }

                    break;
                default:
                    unset($args[$key]);
                    break;
            }
        }

        //Check if the client has cookies enabled
        if($this->forceCookies === true)
            $this->check_cookie();

        //Regenerates the session id for security
        $this->regenerateSession();

        //Generates unique ID for this session
        $this->unique_id = hash($this->hash, $_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR']);

        //If a session timeout ($session_timeout) has been set this checks if they've been inactive to long and 
        //forward them to a specified page when they next make a server request
        /*
        Add a field into the database to track when the user last made a server request, don't use the last logged 
        in one.
        if($session_timeout !== 0)
        {
            $session_life = time() - $_SESSION['login_lastActive'];

            if($session_life > $session_timeout)
            {
                session_destroy();
                header('Location: '.$timeout_page);
            }
            else{ $_SESSION['login_lastActive'] = time(); }
        }
        */
    }

/*
* This is used to set the session variables for users
* 
* $loggedIn (boolean) - Set to true if the user is logged in
* $lastActive (int) - Timestamp of when they they last made a request to the server
* $id (int) - The users id in the database
*/
    private function setLoginSession($loggedIn = NULL, $lastActive = NULL, $id = NULL)
    {
        $lastActive = filter_var($lastActive, FILTER_VALIDATE_INT);
        $id = filter_var($id, FILTER_VALIDATE_INT);

        $_SESSION['login_logged'] = $loggedIn;
        $_SESSION['login_lastActive'] = $lastActive;
        $_SESSION['login_id'] = $id;
    }

/*
* Checks to see if cookies are enabled on the client
* 
* Return true (boolean)
*/
    private function check_cookie()
    {
        //Stops the code being run multiple times as cookies should only need to be checked for once
        if(ini_get('session.use_only_cookies') !== true || ini_get('session.hash_function') !== $this->hash || !isset($_COOKIE['nnspl_cookie']))
        {
            //Starts test to see if cookies are enabled on the client
            if(!isset($_GET['tc']) && !isset($_COOKIE['nnspl_cookie']))
            {
                setcookie("nnspl_cookie", "test", 0);
                header('Location: '.$_SERVER['PHP_SELF'].'?tc=1');
            }

            //If cookies are not enabled the user is forwarded to another page
            if(isset($_GET['tc']) && !isset($_COOKIE['nnspl_cookie']))
                header('Location: '.$this->forceCookies_page);
            //If they are enabled they are set to be secure and true is returned
            else
            {
                $this->setupSecureCookies();
                return true;
            }
        }
    }

/*
* Sets up the cookies on the user to be as secure as possible
*/
    private function setupSecureCookies()
    {
        //Filters to make sure variables are sanitised
        $domain = filter_var($_SERVER['HTTP_HOST'], FILTER_VALIDATE_URL);

        //Sets session cookies to be stored securely
        ini_set('session.use_only_cookies', true);
        ini_set('session.hash_function', $this->hash);

        session_set_cookie_params(0, '/', $domain, isset($_SERVER["HTTPS"]), true);
    }

/*
* This destroys and recreates sessions with a new session id for security reasons, all the previous 
* session data is copied across
*/
    private function regenerateSession()
    {
        //Starts a session and changes the name from the default "PHPSESSID" if it's not already started
        if(!isset($_SESSION)){
            session_name($sessionName);
            session_start();
        }

        //If PHP 5.1 or above is used
        if(strnatcmp(phpversion(),'5.1.0') >= 0)
            session_regenerate_id(true);
        //If less than PHP 5.1 then it will not automatically destroy the previous session.
        else
        {
            // gets current (previous) session
            $previousID = session_id();
            session_regenerate_id();
            // get the new session id
            $newID = session_id();
            // close session related files
            session_write_close();

            // set the old session id
            session_id($previousID);
            // start the old session
            session_start();
            // clear the old session
            session_destroy();
            // save the old session state (destroyed session)
            session_write_close();

            // set the regenerated session id
            session_id($newID);
            // start the new session
            session_start();
        }
    }

/*
* This method handles the actual login of the user
* 
* $db (object) - Database connection
* $username (string) The users entered username - REQUIRED
* $password (string) The users entered password - REQUIRED
* $salt (string) The salt used, if at all - OPTIONAL
*/
    public function login($db, $username = NULL, $password = NULL, $salt = NULL)
    {
        //Holds any errors that are generated and passes them back all together
        $error_array = array();

        //Checks that the username & password have been passed to this method
        if(is_null($username)){ $error_array['username'] = false; }
        if(is_null($password)){ $error_array['password'] = false; }

        //If either of the two above variables aren't passed to this method the error array is returned
        if(!empty($error_array)){ return $error_array; }

        //This adds the 'salt' & 'pepper' to the begining and end of the users entered password and then hashes 
        if(!is_null($salt)){ $password = $salt.$password; }
        if(!is_null($this->pepper)){ $password .= $this->pepper; }
        if(!is_null($this->hash)){ $password = hash($this->hash, $password); }

        //SQL to check if the users login details match against the database records
        $sql = 'SELECT
                    id, 
                    COUNT(*) AS user_account
                FROM
                    '.$this->db_table.'
                WHERE
                    '.$this->username_field.' = ?
                AND
                    '.$this->password_field.' = ?
                AND
                    '.$this->emailAuth_field.' = 1';

        if($prep = $db->prepare($sql))
        {
            $prep->bind_param('ss', $username, $password);

            if($prep->execute())
            {
                $prep->bind_result($id, $user_account);

                $prep->fetch();
                $prep->close();
            }
            else
                die('Error in executing statement, checking user credentials; login method'.$prep->error);
        }
        else
            die('Error in preparing statement, checking user credentials; login method'.$prep->error);

        //If there is a matched record the session variables are set up and unique id created (if set to do so 
        //in the $args array)
        if($user_account === 1)
        {
            //Sets a unique id for this user during this session
            if(!is_null($this->uniqueId_field))
            {
                $setUniqueId_return = $this->setUniqueId($db, $id);

                if($setUniqueId_return !== true)
                    return false;
            }

            $this->setLoginSession('true', time(), $id);

            return true;
        }
        else
            return false;
    }

/*
* This creates a unique ID for each user so after they've logged in each page can reauthenticate them.  
* This works by either creating UID and storing a cookie on the client and storing the UID in the database then 
* comparing them on each page load or if cookies are not being used it stores their IP in the database and comparing 
* it against the users IP.  Both methods hash the unique values before storing them but using cookies is far more secure.
* 
* $db (object) - Connection to the database
* $user_id (int) - The id of the user in the database
*/
    private function setUniqueId($db, $user_id)
    {                              
        //Validates the user id
        $user_id = filter_var($user_id, FILTER_VALIDATE_INT);

        //If either the user id ($user_id) passed in fails the filter or the unique id ($unique_id) created isn't 
        //created for some reason this returns a false indicating the authentication hasn't been successful
        if($user_id === false || empty($user_id)){ return false; }

        //Adds the unique id to the database
        $sql = 'UPDATE 
                    '.$this->db_table.' 
                SET
                    '.$this->db_table.'.'.$this->uniqueId_field.' = ? 
                WHERE 
                    id = ?';

        if($prep = $db->prepare($sql))
        {
            $prep->bind_param('si', $this->unique_id, $user_id);

            if($prep->execute())
            {
                $prep->close();
            }
            else
            {
                die('Error in executing statement, inserting unique ID; setUniqueId'.$prep->error);
            }
        }
        else
        {
            die('Error in preparing statement, inserting unique ID; setUniqueId'.$prep->error);
        }

        return true;
    }

/*
* After the user has sucessfully logged in this is used on subsequent page requests to reauthenticate them without them having to keep 
* entering in their login credentials. It's done using the unique ID created in setUniqueId method which is stored in the database and this 
* creates unique ID again in the same way and compares it to what is in the database.
*/
    public function reauthenticateUser()
    {
        //SQL to check check the newly generated UID to what has been originally saved in the database
        $sql = 'SELECT
                    '.$this->uniqueId_field.'
                FROM
                    '.$this->db_table.'
                WHERE
                    id = ?';

        //Gets the unique ID stored in the databae
        if($prep = $db->prepare($sql))
        {
            $prep->bind_param('i', $_SESSION['id']);

            if($prep->execute())
            {
                $prep->bind_result($uid);

                $prep->fetch();
                $prep->close();
            }
            else
                die('Error in executing statement, reauthenticating user; reauthenticateUser method'.$prep->error);
        }
        else
            die('Error in preparing statement, reauthenticating user; reauthenticateUser method'.$prep->error);

        //Compares the newly generated unique ID with what was in the database
        if($uid === $this->unique_id)
            return true;
        else
            return false;
    }

    public function logout(){
        unset($_COOKIE['nnspl_cookie']);
        session_destory();
    }
}
?>